iT邦幫忙

2024 iThome 鐵人賽

DAY 3
1
Software Development

用 NestJS 闖蕩微服務!系列 第 3

[用NestJS闖蕩微服務!] DAY03 - 初探微服務應用程式(下)

  • 分享至 

  • xImage
  •  

客戶端 (Client)

上一篇在介紹 NestJS 的微服務應用程式以及相關概念,那要如何用 NestJS 與微服務應用程式溝通呢?NestJS 有為此設計了 客戶端(Client) 的相關機制。客戶端的定義與微服務應用程式差不多,指的是 使用與 HTTP 協定不同傳輸層進行溝通的客戶端

ClientProxy

NestJS 在客戶端也下了不少功夫,使用 Proxy Pattern 的方式設計了 ClientProxy 這個類別,背後會根據指定的 Transporter 來發送訊息,如此一來,無論是哪一種 Transporter 都可以用相同的方式來處理。

NestJS Microservice Client Concept

前置作業

透過 NestCLI 產生一個專案:

$ nest new <PROJECT_NAME>

專案產生完之後,需額外安裝微服務應用程式相關套件:

$ npm install @nestjs/microservices

透過下方指令啟動應用程式:

$ npm run start:dev

產生 ClientProxy

產生 ClientProxy 的方式共有三種,分別是:ClientsModuleClientProxyFactoryClient Decorator

ClientsModule

透過 ClientsModuleregister 靜態方法可以針對不同 Transporter 來建立多個 ClientProxy,並將其作為 Provider,即可用注入的方式取得。下方是產生使用 TCP Transporter 的 ClientProxy 範例程式碼:

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
// ...

@Module({
  // ...
  imports: [
    ClientsModule.register([
      {
        name: 'SAMPLE_SERVICE',
        transport: Transport.TCP,
        options: {
          host: '0.0.0.0',
          port: 3333,
        },
      }
    ])
  ]
})
export class AppModule {}

register 帶入的參數為一個物件陣列,裡面的物件有以下三個屬性可以設置:

  • name:用來當作該 ClientProxytoken,是一定要帶的參數。
  • transporter:指定該 ClientProxy 所使用的 Transporter,預設是 TCP Transporter。
  • options:根據指定的 Transporter 會有不同的設置,設定的格式會跟建立微服務應用程式時,針對不同 Transporter 的相關設置相同。

針對 ClientsProxy 的設定內容,有可能會來自設定檔,面對這種情境,可以使用 ClientsModule 提供的非同步處理方案,將 register 改成 registerAsync 靜態方法即可,該靜態方法帶入的參數為一個物件,該物件有一個 clients 屬性,該屬性的型別為物件陣列,可以用來設置多組 ClientProxy

假如現在有一個環境變數檔 .env,內容如下:

SAMPLE_SERVICE_HOST=0.0.0.0
SAMPLE_SERVICE_PORT=3333

此時透過 ConfigModule 來管理這些環境變數,再搭配 registerAsync 就可以讀取環境變數來產生 ClientProxy。下方為範例程式碼:

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';
// ...

@Module({
  // ...
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ClientsModule.registerAsync({
      clients: [
        {
          name: 'SAMPLE_SERVICE',
          inject: [ConfigService],
          useFactory: (configService: ConfigService) => ({
            transport: Transport.TCP,
            options: {
              host: configService.get('SAMPLE_SERVICE_HOST'),
              port: configService.get('SAMPLE_SERVICE_PORT')
            }
          })
        }
      ],
    })
  ]
})
export class AppModule {}

提醒:關於 ConfigModule 可以參考之前系列文的 Configuration 章節

ClientsProxy 產生後,可以透過 @Inject 裝飾器將其注入,以下方程式碼為例,在 AppController 注入 tokenSAMPLE_SERVICE 的 Provider,該 Provider 即先前指定 nameSAMPLE_SERVICEClientProxy

import { Controller } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Controller()
export class AppController {
  constructor(
    @Inject('SAMPLE_SERVICE')
    private readonly client: ClientProxy
  ) {}
  // ...
}

ClientProxyFactory

使用 ClientProxyFactory 可以在不使用 ClientsModule 的情況下產生 ClientProxy,透過自訂 Provider 的方式即可輕鬆使用。以下方程式碼為例,使用 useFactory 注入 ConfigService 來讀取 ConfigModule 管理的環境變數,並產生 ClientProxy

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';
// ...

@Module({
  // ...
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
  ],
  providers: [
    {
      provide: 'SAMPLE_SERVICE',
      useFactory: (configService: ConfigService) => {
        return ClientProxyFactory.create({
          transport: Transport.TCP,
          options: {
            host: configService.get('SAMPLE_SERVICE_HOST'),
            port: configService.get('SAMPLE_SERVICE_PORT'),
          },
        });
      },
      inject: [ConfigService],
    }
  ],
})
export class AppModule {}

補充:去看 NestJS 的原始碼會發現,不論是 register 還是 registerAsync,它們產生 ClientProxy 都是基於 ClientProxyFactory 來產生的。

Client Decorator

使用 Client 裝飾器一樣可以在不使用 ClientsModule 的情況下產生 ClientProxy。下方為範例程式碼,在 AppService 中使用 Client 裝飾器:

import { Inject, Injectable } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';

@Injectable()
export class AppService {
  @Client({
    transport: Transport.TCP,
    options: {
      host: '0.0.0.0',
      port: 3333,
    },
  })
  private readonly client: ClientProxy;

  // ...
}

但這種產生 ClientProxy 的方式並 不推薦 使用,原因是產生出來的 ClientProxy 不會 被 IoC Container 管理,所以每使用一次就會建立一個實例。

傳送訊息

在產生完 ClientProxy 之後,就可以透過它來跟微服務應用程式進行溝通,前面有提到,訊息模式有分 Request-response 與 Event-based 兩種,故ClientProxy 支援這兩種傳訊息的方式。

Request-response

ClientProxy 提供了 send 方法來傳送訊息,並設計回傳值為 Observable 來等待回應,該方法需帶入兩個參數:

  • pattern:需與微服務應用程式在 @MessagePattern 裝飾器中設置的 Pattern 相同。
  • data:要傳送的資料,具體內容須符合該 API 規格。

注意send 方法並不是呼叫了就會傳送訊息,要訂閱回傳的 Observable 才會送出,當然也可以選擇在 Controller 的 Handler 將 Observable 回傳,讓 NestJS 自動訂閱它。

下方為範例程式碼,在 AppController 注入 ClientProxy,並透過 send 傳送 Pattern 為 { cmd: 'hello' } 的訊息:

import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Controller()
export class AppController {
  constructor(
    @Inject('SAMPLE_SERVICE')
    private readonly client: ClientProxy
  ) {}
    
  @Get()
  sayHello() {
    return this.client.send({ cmd: 'hello' }, 'HAO');
  }
}

微服務應用程式的範例程式碼如下:

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class AppController {
  @MessagePattern({ cmd: 'hello' })
  sayHello(data: string) {
    return `Hello, ${data}`;
  }
}

透過 Postman 使用 GET 方法存取 http://localhost:3000,會看到下方的結果:

Request-response-result

串流

Request-response 支援使用串流的方式回傳訊息,如果只是單純將 send 回傳的 Observable 讓 NestJS 自動訂閱,那可能結果會不如預期,原因是 NestJS 會等到該 Observable 進入 complete 狀態再 使用最後一個值做為回傳值,如果說要等到串流完成並將串流過程中的所有回傳值都做處理的話,需要使用 toArray 這個 RxJS 的 operator,它會匯總所有回傳值直到進入 complete 狀態:

import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { toArray } from 'rxjs';

@Controller()
export class AppController {
  constructor(
    @Inject('SAMPLE_SERVICE')
    private readonly client: ClientProxy
  ) {}
    
  @Get()
  sayHello() {
    return this.client.send({ cmd: 'hello' }, 'HAO').pipe(toArray());
  }
}

微服務應用程式的範例程式碼如下:

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { delay, from, map } from 'rxjs';

@Controller()
export class AppController {
  @MessagePattern({ cmd: 'hello' })
  sayHello(data: string) {
    return from([`${data} 1`, `${data} 2`]).pipe(
      map((res) => `Hello, ${res}`),
      delay(2000)
    );
  }
}

透過 Postman 使用 GET 方法存取 http://localhost:3000,預期會得到 ["Hello, HAO 1", "Hello, HAO 2"]

Request-response-stream-result

處理 Timeout

在微服務的架構下,有時其他服務回應速度過慢會導致依賴它的服務回應速度也跟著變慢,甚至會造成非常長時間的無回應狀態,此時就需要設置 Timeout,RxJS 有提供 timeout 這個 operator,當達到指定時間還沒有收到回應,就會拋出錯誤。下方範例程式碼設置了 5000 毫秒的 Timeout 時間:

import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { timeout } from 'rxjs';

@Controller()
export class AppController {
  constructor(
    @Inject('SAMPLE_SERVICE')
    private readonly client: ClientProxy
  ) {}
    
  @Get()
  sayHello() {
    return this.client.send({ cmd: 'hello' }, 'HAO').pipe(timeout(5000));
  }
}

微服務應用程式的範例程式碼如下:

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { delay, of } from 'rxjs';

@Controller()
export class AppController {
  @MessagePattern({ cmd: 'hello' })
  sayHello(data: string) {
    return of(`Hello, ${data}`).pipe(
      delay(6000)
    );
  }
}

透過 Postman 使用 GET 方法存取 http://localhost:3000,預期會得到錯誤:

Request-response timeout

Event-based

ClientProxy 提供 emit 方法來傳送訊息,該方法需帶入兩個參數:

  • pattern:需與微服務應用程式在 @EventPattern 裝飾器中設置的 Pattern 相同。
  • data:要傳送的資料,具體內容須符合該 API 規格。

下方為範例程式碼,在 AppController 注入 ClientProxy,並透過 emit 傳送 Pattern 為 order.created 的訊息:

import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Controller()
export class AppController {
  constructor(
    @Inject('SAMPLE_SERVICE')
    private readonly client: ClientProxy
  ) {}
    
  @Get('orderCreated')
  onOrderCreated() {
    this.client.emit('order.created', { name: 'test' });
    return {};
  }
}

微服務應用程式的範例程式碼如下:

import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';

@Controller()
export class AppController {
  @EventPattern('order.created')
  onOrderCreated(order: { name: string }) {
    console.log(order);
  }
}

透過 Postman 使用 GET 方法存取 http://localhost:3000/orderCreated,在微服務應用程式的終端機會看到 { name: 'test' }

Event-based-result

ClientProxy 主動建立連線

ClientProxy 背後會處理與微服務應用程式的連線,正常情況下,會在第一次送出訊息時建立連線,並且之後每次傳送訊息時都使用該連線,所以可以說 ClientProxy惰性(Lazy) 的。

如果想要在應用程式啟動時,確保一定有連上服務,可以運用 OnApplicationBootstrap 這個 Lifecycle Hook 搭配 ClientProxyconnect 方法,如此一來,當沒辦法建立起連線,自身的服務也不會啟動。

提醒:關於 Lifecycle Hooks 可以參考之前系列文的 Lifecycle Hooks 章節

下方為範例程式碼,在 AppModule 實作 OnApplicationBootstrap 介面,在 onApplicationBootstrap 呼叫 ClientProxyconnect 方法並等待其完成:

// ...
@Module({
  // ...
})
export class AppModule implements OnApplicationBootstrap {
  constructor(
    @Inject('SAMPLE_SERVICE')
    private readonly client: ClientProxy
  ) {}

  async onApplicationBootstrap() {
    await this.client.connect();
  }
}

小結

NestJS 實作了與微服務應用程式溝通的客戶端,為了滿足不同的傳輸方式,客戶端採用 Transporter 與 Proxy Pattern 來產生 ClientProxy,盡可能地保持相同的開發體驗與微服務應用程式溝通。

ClientProxy 產生的方式有三種,分別是:透過 ClientsModule 產生、使用自訂 Provider 的技巧搭配 ClientProxyFactory 來產生、使用 @Client 裝飾器產生。但並不推薦使用 @Client 裝飾器,因為產生出來的 ClientProxy 不會被 IoC Container 管理。

ClientProxy 可以使用 Request-response 與 Event-based 訊息模式來跟微服務應用程式進行溝通,用 send 方法即可傳送訊息並等待回應、用 emit 方法即可單方面傳送訊息給微服務應用程式。

ClientProxy 是惰性的,預設狀況下,會在第一次跟微服務應用程式傳送訊息時建立連線,如果希望在應用程式啟動時確保一定有建立連線,可以使用 OnApplicationBootstrap 這個 Lifecycle Hook,在這裡呼叫 ClientProxyconnect 方法並等待它完成。


上一篇
[用NestJS闖蕩微服務!] DAY02 - 初探微服務應用程式(上)
下一篇
[用NestJS闖蕩微服務!] DAY04 - Redis Transporter
系列文
用 NestJS 闖蕩微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
yyfang0616
iT邦新手 5 級 ‧ 2024-09-22 10:03:53

Hi,很喜歡你推出的 nestjs 系列介紹,有個問題有點好奇,上面文章提到 ClientProxy 產生的方式有三種:透過 ClientsModule、自訂 Provider、@Client decorator

想知道 透過 ClientsModule自訂 Provider 這兩種方式在後續有什麼差異嗎?應該如何選用?

謝謝!

HAO iT邦研究生 1 級 ‧ 2024-09-22 17:04:51 檢舉

Hi 你好,
很高興你喜歡我的文章!

針對 ClientsModule 與自訂 Provider 的差異,有兩個:

  1. ClientsModule 有提供 isGlobal 的選項,可以將 ClientProxy 提升到全域。
  2. 透過 ClientsModule 產生的 ClientProxy 會在 onApplicationShutdown 這個 Lifecycle Hook 呼叫時執行其 close 方法,這部分可以參考原始碼的設計。

上述的特性都只有 ClientsModule 才有,如果沒有特殊需求,我會建議使用 ClientsModule 來產生。

我要留言

立即登入留言